{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Задача Python Solo Hackathon \"Board Game 5k\"\n",
    "\n",
    "Перед Вами стоит интересная задача\n",
    "\n",
    "Корректно определить название игры по ее описанию и сделать это для 5 000 игр!\n",
    "\n",
    "Ваша команда уже поработала над задачей, собрала нужные датасеты и реализовала MVP решение. Вам предстоит попробовать улучшить текущее решение или создать свое.\n",
    "\n",
    "В конце недели мы выберем тройку победителей, вручим приятный подарки и посмотрим примеры решений! \n",
    "\n",
    "## Описание данных:\n",
    "\n",
    "В наличии есть несколько датасетов:\n",
    "\n",
    "- review.csv (246 тыс. примеров) датасет в котором содержатся **отзывы пользователей** (поле comment) и название игры (game)\n",
    "- games.csv (5 тыс примеров) - датасет в котором содержится **описание игры** (desc) и название игры (game)\n",
    "- games_submission.csv (5 тыс примеров) - содержатся описания игр (desc) для которых нужно определить название\n",
    "\n",
    "## Ваша цель:\n",
    "\n",
    "Построить нужное кол-во ML моделей и классифицировать все игры в датасете games_submission.csv (т.е. связать описание игры с ее именем). Как результат Вашей работы нужно прислать  файл для оценки качества классификации YOUR_NAME_01.csv в бот СберЧата @pythonhackaton.\n",
    "\n",
    "Вы можете улучшать своё решение и повышать свой результат столько раз, сколько захотите. В итоговом лидерборде будет учитываться лучшее решение. \n",
    "\n",
    "Ключевой метрикой для данной задачи является доля правильных ответов (accuracy). \n",
    "\n",
    "## Правила:\n",
    "\n",
    "1. Обязательно сохраните нотебук со своим лучшим решением. Организаторы могут запросить у вас его.\n",
    "2. Нотебук с вашим решением должен воспроизводиться\n",
    "3. Можно использовать открытые предобученные модели, но все дообучение должно происходить в данном нотебуке\n",
    "4. Имена всех игр гарантированно содержатся в датасете review.csv\n",
    "5. Сторонние датасеты использовать нельзя\n",
    "6. Решения принимаются до 25 апреля 16:00 (мск) \n",
    "\n",
    "## Подведение итогов:\n",
    "\n",
    "Итоги подведем в прямом эфире 26 апреля в 14:00.\n",
    "Выберем 3-х победителей и вручим им приятные подарки. \n",
    "\n",
    "## Контакты для связи:\n",
    "\n",
    "Вопросы, связанные с заданием: Дмитрий Сысоев (sysoev.d.se@sberbank.ru)\n",
    "Вопросы, связанные с орг.моментами: Юрченко Павел (payurchenko@sberbank.ru)\n",
    "\n",
    "# Install env\n",
    "\n",
    "```bash\n",
    "pip install pandas numpy scikit-learn tqdm jupyterlab\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import List\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.feature_extraction.text import TfidfVectorizer\n",
    "from sklearn.metrics.pairwise import cosine_distances\n",
    "\n",
    "import tqdm\n",
    "\n",
    "\n",
    "def accuracy_score(true: List[str], predict: List[str]) -> float:\n",
    "    \"\"\"\n",
    "    Оценивает точность\n",
    "    \"\"\"\n",
    "    return (np.array(predict) == np.array(true)).astype(int).mean().round(3)\n",
    "\n",
    "\n",
    "def predict_game(model: Pipeline,\n",
    "                 games_with_review: pd.DataFrame,\n",
    "                 games_unknown: pd.Series) -> List[str]:\n",
    "    \"\"\"\n",
    "    Возращает список имен игр на основе описания в датасете games_unknown\n",
    "    В датасет games_with_review содержатся отзывы пользователей\n",
    "\n",
    "    Если игры из которых можно выбирать закончатся, будет возращать имя None\n",
    "    \"\"\"\n",
    "\n",
    "    # tf-idf матрица с отзывами пользователей\n",
    "    embedding_all = pd.DataFrame(\n",
    "        model.transform(games_with_review['comment__join']).todense(),\n",
    "        columns=model.get_feature_names_out(),\n",
    "        index=games_with_review.index)\n",
    "\n",
    "    # tf-idf матрица с описаниями игр\n",
    "    embedding_unknown = pd.DataFrame(\n",
    "        model.transform(games_unknown).todense(),\n",
    "        columns=model.get_feature_names_out(),\n",
    "        index=games_unknown.index)\n",
    "\n",
    "    # индекс с играми которые нужно определить\n",
    "    games_to_find_index = games_unknown.index.tolist()\n",
    "    # индекс с доступными играми\n",
    "    games_bag_index = games_with_review.index.tolist()\n",
    "\n",
    "    predict = []\n",
    "    for index in tqdm.tqdm(games_to_find_index):\n",
    "\n",
    "        if not games_bag_index:\n",
    "            # если доступных игр нет возращаем None\n",
    "            predict.append('None')\n",
    "            continue\n",
    "\n",
    "        # объект к которому будем искать имя\n",
    "        ref = embedding_unknown.loc[index]\n",
    "        games_proposal = games_with_review.loc[games_bag_index]\n",
    "\n",
    "        # выбираем игру ближайшую к искомому объекту по косинусной мере\n",
    "        cos_sim = cosine_distances(\n",
    "            [ref], embedding_all.loc[games_proposal.index])\n",
    "        selected_loc = games_proposal.index[np.argmin(cos_sim)]\n",
    "        selected_game = games_with_review.loc[selected_loc]['game']\n",
    "        # добавляем имя игры в предсказание\n",
    "        predict.append(selected_game)\n",
    "        # исключаем игру из поиска для след итераций\n",
    "        games_bag_index.remove(selected_loc)\n",
    "\n",
    "    return predict\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "review.shape: (246946, 2)\n",
      "games.shape: (4975, 2), games_sub.shape: (5134, 1)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/w3/p23189px7jv__jhb3xkm50p9hfr8c5/T/ipykernel_6920/3661825707.py:5: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.\n",
      "The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.\n",
      "\n",
      "For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.\n",
      "\n",
      "\n",
      "  review['comment'].fillna('', inplace=True)\n",
      "/var/folders/w3/p23189px7jv__jhb3xkm50p9hfr8c5/T/ipykernel_6920/3661825707.py:11: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.\n",
      "The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.\n",
      "\n",
      "For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.\n",
      "\n",
      "\n",
      "  games['desc'].fillna('', inplace=True)\n",
      "/var/folders/w3/p23189px7jv__jhb3xkm50p9hfr8c5/T/ipykernel_6920/3661825707.py:12: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.\n",
      "The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.\n",
      "\n",
      "For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.\n",
      "\n",
      "\n",
      "  games_sub['desc'].fillna('', inplace=True)\n"
     ]
    }
   ],
   "source": [
    "# читаем датасеты с отзывами\n",
    "review = (\n",
    "    pd.read_csv('review5k.csv')\n",
    ")\n",
    "review['comment'].fillna('', inplace=True)\n",
    "print(f\"review.shape: {review.shape}\")\n",
    "\n",
    "# читаем датасеты с играми\n",
    "games = pd.read_csv('games5k.csv')\n",
    "games_sub = pd.read_csv('games5k_submission.csv')\n",
    "games['desc'].fillna('', inplace=True)\n",
    "games_sub['desc'].fillna('', inplace=True)\n",
    "print(f\"games.shape: {games.shape}, games_sub.shape: {games_sub.shape}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/var/folders/w3/p23189px7jv__jhb3xkm50p9hfr8c5/T/ipykernel_6920/2796244439.py:14: FutureWarning: A value is trying to be set on a copy of a DataFrame or Series through chained assignment using an inplace method.\n",
      "The behavior will change in pandas 3.0. This inplace method will never work because the intermediate object on which we are setting values always behaves as a copy.\n",
      "\n",
      "For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' or df[col] = df[col].method(value) instead, to perform the operation inplace on the original object.\n",
      "\n",
      "\n",
      "  games_desc_full['desc'].fillna('', inplace=True)\n",
      "100%|██████████| 4975/4975 [03:43<00:00, 22.23it/s] "
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "train accuracy: 0.106\n",
      "count: None 0\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# собираем все отзывы по игре в одну большую строку\n",
    "games_and_desc = (\n",
    "    review\n",
    "    .groupby(['game'])\n",
    "    .agg({'comment': [' '.join]})\n",
    "    .reset_index()\n",
    ")\n",
    "# переимовываем колонки\n",
    "games_and_desc.columns = [\n",
    "    x + '__' + y if y else x for x, y in games_and_desc.columns.ravel()]\n",
    "\n",
    "# объединяем описания игр из games и games_sub\n",
    "games_desc_full = pd.concat([games_sub[['desc']], games[['desc']]], axis=0)\n",
    "games_desc_full['desc'].fillna('', inplace=True)\n",
    "\n",
    "# готовим Pipeline\n",
    "model = Pipeline([\n",
    "    # нам нужен только tf-idf вектор\n",
    "    ('tfidf', TfidfVectorizer(stop_words='english', max_features=2000)),\n",
    "])\n",
    "# обучаем нашу модель на всех описаниях игр!\n",
    "model.fit(games_desc_full['desc'], None)\n",
    "\n",
    "#\n",
    "# Далее идет скоринг на обучающем датасете\n",
    "# и оценка получившегося качества модели\n",
    "#\n",
    "# выбираем игры которые есть в games.csv\n",
    "games_with_review = (\n",
    "    games_and_desc\n",
    "    [games_and_desc['game'].isin(games['game'].unique().tolist())]\n",
    ")\n",
    "\n",
    "# для каждого описания игры находим игру\n",
    "# отзывы которой имеют минимальное расстояние (косинусное расстояние)\n",
    "predict_train = predict_game(\n",
    "    model=model,\n",
    "    games_with_review=games_with_review[['game', 'comment__join']],\n",
    "    games_unknown=games['desc']\n",
    ")\n",
    "\n",
    "# оцениваем качество на обучающем наборе\n",
    "train_accuracy = accuracy_score(\n",
    "    true=games['game'].tolist(),\n",
    "    predict=predict_train)\n",
    "print(f\"train accuracy: {train_accuracy}\")\n",
    "\n",
    "# кол-во описаний для которых не нашлось игр\n",
    "count_none = sum([1 for x in predict_train if x == 'None'])\n",
    "print(f\"count: None {count_none}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████| 5134/5134 [04:04<00:00, 21.04it/s] "
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "count: None 0\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "#\n",
    "# Далее идет скоринг на отложенной выборке\n",
    "# и сохраняем предсказания в файл\n",
    "\n",
    "# формируем список игр которые нам нужно определить\n",
    "games_unknown_list = list(\n",
    "    set(games_and_desc['game'].unique()) - set(games['game'].unique()))\n",
    "\n",
    "# выбираем игры которых нет в games.csv\n",
    "games_with_review = (\n",
    "    games_and_desc\n",
    "    [games_and_desc['game'].isin(games_unknown_list)]\n",
    ")\n",
    "\n",
    "# для каждого описания игры находим игру\n",
    "# отзывы которой имеют минимальное расстояние (косинусное расстояние)\n",
    "predict_test = predict_game(\n",
    "    model=model,\n",
    "    games_with_review=games_with_review[['game', 'comment__join']],\n",
    "    games_unknown=games_sub['desc']\n",
    ")\n",
    "\n",
    "# кол-во описаний для которых не нашлось игр\n",
    "count_none = sum([1 for x in predict_test if x == 'None'])\n",
    "print(f\"count: None {count_none}\")\n",
    "\n",
    "# формируем файл с предсказанием на тесте\n",
    "games_sub['game'] = predict_test\n",
    "submission = games_sub[['game']]\n",
    "if not submission.shape == (5134, 1):\n",
    "    raise ValueError('Error in submission dataset shape')\n",
    "\n",
    "# сохраняем файл с указанием Имени, Фамилии и порядковый номер посылки\n",
    "submission.to_csv('Ivanov-Ivan-01.csv', index=None)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e4a9f41",
   "metadata": {},
   "source": [
    "# Можно отправить свое решение боту https://sberchat.sberbank.ru/@pythonhackaton на проверку"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8af532da",
   "metadata": {},
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
